package com.bantang;

import cn.hutool.core.date.StopWatch;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import com.bantang.model.JudgeInfo;
import com.bantang.model.SandBoxParam;
import com.bantang.model.SandBoxVO;
import com.bantang.utils.CompileCodeUtil;
import com.bantang.utils.ExecResult;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallback;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.command.ExecStartResultCallback;


import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class JavaDockerCodeSandbox implements CodeSandBox {

    private static boolean judge = true;

    public static void main(String[] args) {

        CodeSandBox codeSandBox = new JavaDockerCodeSandbox();
        SandBoxParam sandBoxParam = new SandBoxParam();
        sandBoxParam.setLanguage("java");

        String code = ResourceUtil.readStr("testCode/Solve.java", StandardCharsets.UTF_8);

        sandBoxParam.setCode(code);
        sandBoxParam.setInput(Arrays.asList("1 2", "3 4"));

        SandBoxVO sandBoxVO = codeSandBox.executeCodeSandBox(sandBoxParam);

        System.out.println(sandBoxVO);
    }

    /**
     * docker 代码沙箱
     *
     * @param sandBoxParam
     * @return
     */
    @Override
    public SandBoxVO executeCodeSandBox(SandBoxParam sandBoxParam) {
        SandBoxVO sandBoxVO = new SandBoxVO();
        try {
            //获取代码 输入用例
            String code = sandBoxParam.getCode();
            List<String> input = sandBoxParam.getInput();

            //获取项目根路径
            String userDir = System.getProperty("user.dir");
            //在根目录创建文件夹
            String codePathName = userDir + File.separator + "tempCode";
            if (!FileUtil.exist(codePathName)) {
                FileUtil.mkdir("tempCode");
            }
            //把用户代码隔离 写入新的文件夹 Main.java
            String userCodePath = codePathName + File.separator + UUID.randomUUID();
            String userCodePathName = userCodePath + File.separator + "Solve.java";
            File userFile = FileUtil.writeString(code, userCodePathName, StandardCharsets.UTF_8);

            //编译代码 生成Main.class
            String codeCmd = String.format("javac -encoding utf-8 %s", userFile.getAbsolutePath());
            ExecResult execResult = CompileCodeUtil.compileCode(codeCmd);

            //1)编译错误
            if (!"success".equals(execResult.getMessage())) {
                sandBoxVO.setBoxMessage("编译错误");
                return sandBoxVO;
            }

            //-------------------------docker 实现代码沙箱-----------------------

            //1.创建docker客户端
            DockerClient dockerClient = DockerClientBuilder.getInstance().build();

            //1.1如果是第一次初始化 拉取java环境 镜像
            String image = "openjdk:8-alpine";//alpine 是轻量版本
            if (judge) {
                //拉取镜像
                dockerClient.pullImageCmd(image);
                judge = false;
            }

            //1.2 创建容器cmd 命令预设
            CreateContainerCmd containerCmd = dockerClient.createContainerCmd(image);

            //1.3 通过HostConfig配置 限制最大内存 并把java编译好的文件夹映射到docker根目录/app 下
            HostConfig hostConfig = new HostConfig();
            //把本地路径映射到docker/app下
            hostConfig.setBinds(new Bind(userCodePath, new Volume("/app")));
            //限制最大内存为100m
            hostConfig.withMemory(100 * 1000 * 1000L);
            //hostConfig.withSecurityOpts(Arrays.asList("secomp=安全管理配置"));

            //2 绑定容器配置
            CreateContainerResponse createContainerResponse = containerCmd
                    .withCmd("bash")
                    .withHostConfig(hostConfig)//设置host配置
                    .withNetworkDisabled(true)//限制网络
                    .withReadonlyRootfs(true)//限制不能向根目录写文件
                    .withAttachStdin(true)//withAttach 为了得到控制台输出 以防万一
                    .withAttachStderr(true)
                    .withAttachStdout(true)
                    .withTty(true)
                    .exec();

            //3 获取容器id
            String containerId = createContainerResponse.getId();
            System.out.println("容器id" + containerId);
            //4 启动容器
            dockerClient.startContainerCmd(containerId).exec();


            //-------------------------启动容器 需要多次吗？
            //todo 这里放静态块里面 只执行一次 拉镜像和启动容器 包括配置限制和映射 但是映射路径 和 containerId 如何处理？

            long time = 0L;
            //docker执行java程序cmd： docker exec keen_blackwell java -cp /app Solve 1 3
            System.out.println("docker执行java程序cmd： docker exec dreamy_tesla java -cp /app Main 1 3");

            //5 init 包装返回VO
            ExecResult runResult = new ExecResult();
            JudgeInfo judgeInfo = new JudgeInfo();
            List<String> outputValues = new ArrayList<>();
            Long maxTime = 0L;

            //6 多次执行 input 获取output 这里在已经运行的容器里/bin/bash里执行
            for (String inputArray : input) {

                StopWatch stopWatch = new StopWatch();
                String[] args = inputArray.split(" ");
                String[] cmdArray = ArrayUtil.append(new String[]{"docker", "exec", "java", "-cp", "/app", "Solve"}, args);
                //1 配置运行java代码 前置条件 这里创建exec 哪个控制台？
                ExecCreateCmdResponse exec = dockerClient.execCreateCmd(containerId)
                        .withCmd(cmdArray) //设置命令行
                        .withAttachStdin(true) //withAttach 为了得到控制台输出 以防万一
                        .withAttachStderr(true)
                        .withAttachStdout(true)
                        .exec();
                //获取exec的 id
                String execId = exec.getId();

                //限制是否超时 默认是超时
                final boolean[] isComplete = {true};

                //2 配置执行回调函数
                ExecStartResultCallback execStartResultCallback = new ExecStartResultCallback() {

                    //运行完成 改变超时判断 如果在指定时间运行不完 状态不会改变
                    @Override
                    public void onComplete() {
                        isComplete[0] = false;
                        super.onComplete();
                    }

                    //获取输出结果
                    @Override
                    public void onNext(Frame frame) {

                        StreamType type = frame.getStreamType();
                        if (StreamType.STDERR.equals(type)) {
                            System.out.println("错误输出结果:" + new String(frame.getPayload()));
                            runResult.setMessage(new String(frame.getPayload()));
                        } else {
                            System.out.println("输出结果:" + new String(frame.getPayload()));
                            outputValues.add(new String(frame.getPayload()));
//                            runResult.setResult(new String(frame.getPayload()));
                        }
                        super.onNext(frame);
                    }
                };


                try {
                    stopWatch.start();
                    //3 开始执行代码
                    System.out.println("开始执行代码");
                    dockerClient.execStartCmd(execId)
                            .exec(execStartResultCallback)
                            .awaitCompletion(5000L, TimeUnit.MICROSECONDS);//控制超时 等待5秒执行下一步

                    if (isComplete[0]) {
                        //超时
                    }
                    stopWatch.stop();
                    time = stopWatch.getLastTaskTimeMillis();

                } catch (InterruptedException e) {
                    System.out.println("程序执行异常");
                    throw new RuntimeException(e);
                }

                runResult.setTime(time);

            }


            //最大内存值
            final long[] maxMemory = {0L};
            //获取占用内存
            StatsCmd statsCmd = dockerClient.statsCmd(containerId);
            ResultCallback<Statistics> exec = statsCmd.exec(new ResultCallback<Statistics>() {
                @Override
                public void onNext(Statistics statistics) {
                    System.out.println("内存占用" + statistics.getMemoryStats().getUsage());
                    maxMemory[0] = Math.max(statistics.getMemoryStats().getUsage(), maxMemory[0]);
                    //获取占用内存
                    judgeInfo.setMemory(maxMemory[0]);
                }

                @Override
                public void onStart(Closeable closeable) {
                }

                @Override
                public void onError(Throwable throwable) {
                }

                @Override
                public void onComplete() {
                }

                @Override
                public void close() throws IOException {
                }
            });
//            exec.close();

            //整理结果 删除文件 停止容器？
            sandBoxVO.setOutput(outputValues);
            judgeInfo.setTime(maxTime);

            sandBoxVO.setJudgeInfo(judgeInfo);
            FileUtil.del(userCodePath);

        } catch (Exception e) {
            System.out.println("执行异常");
            e.printStackTrace();
        }
        return sandBoxVO;
    }
}
